/**
 * FreeZzaph.java
 * 
 * FreeZzaph is free software; you can redistribute it
 * and/or modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * FreeZzaph is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.
 */
package freezzaph;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.regex.PatternSyntaxException;

import freezzaph.exceptions.PluginException;
import freezzaph.plugins.Category;
import freezzaph.plugins.CategoryList;
import freezzaph.plugins.FreeZzaphPlugin;


/**
 * <p><strong>FreeZzaph<br>
 * &nbsp;&nbsp;&nbsp;&nbsp;— because geeks need porn too.</strong>
 * 
 * <p>Application based on the idea behind Zzaph,
 * see <a href="http://www.zzaph.com">Zzaph.com</a>.
 * This application, using a plug-in interface, will
 * scour the web for porn and download it for you,
 * without  you needing to take the time searching
 * for it, opening up the containing pages and
 * downloading the material manually.
 * 
 * <p>This will allow you to use your time more
 * responsibly, letting the computer do all the boring
 * work of finding and downloading, and you just have
 * to perform the good part — looking at it!
 * 
 * @author FreeZzaph
 */
public final class FreeZzaph {
	
	public static final int majorVersion = 0;
	public static final int minorVersion = 1;
	
	private static HashMap<String, FreeZzaphPlugin> pluginList;
	private static FreeZzaphPlugin currentPlugin = null;

	public static final void main(String[] args) {
		final PrintStream o = System.out;
		final BufferedReader i = new BufferedReader(new InputStreamReader(System.in));
		
		// Write introduction
		o.printf("FreeZzaph v%d.%d Copyright © 2008 FreeZzaph\n", majorVersion, minorVersion);
		o.printf("This program comes with ABSOLUTELY NO WARRANTY; for details see\n");
		o.printf("the COPYING file.\n\n");
		
		// Load the plugins
		o.printf("Loading plugins...");
		FreeZzaphPluginFactory pf = new FreeZzaphPluginFactory();
		try {
			pf.loadPlugins();
			pluginList = pf.getPluginList();
			o.printf("done!\n\n");
		} catch (PluginException e) {
			o.printf("failed with message '%s'!\n\n", e.getMessage());
		}
		
		// Main program loop
		while (true) {
			
			// Write prompt
			if (currentPlugin == null) {
				o.printf("FreeZzaph> ");
			} else {
				o.printf("FreeZzaph/%s> ", currentPlugin.getPluginName());
			}
			
			// Receive input
			String input;
			try {
				input = i.readLine();
			} catch (IOException e) {
				System.err.printf("I/O error occured while reading from keyboard. Detail:\n%s", e.getMessage());
				break;
			}
			
			if (input.equals("quit") || input.equals("q") || input.equals("exit")) {
				// Quitting: Leaving program loop
				break;
			} else if (input.equals("list")) {
				// List available plugins
				listPlugins(o);
			} else if (input.equals("help")) {
				// Show help
				showHelp(o);
			} else {
				// More complex parsing
				String[] cmd = parseParameters(input);
				if (cmd == null) {
					o.printf("The command '%s' is an invalid command\n", input);
					continue;
				}
				
				if (cmd[0].equals("select") || cmd[0].equals("use")) {
					// Select plugin
					selectPlugin(o, cmd);
				} else if (cmd[0].equals("category")) {
					// List categories
					listCategories(o, cmd);
				} else if (cmd[0].equals("fetch")) {
					// Download files
					fetch(o, cmd);
				} else {
					// Command not recognized
					o.printf("Command '%s' is not recognized.\n", cmd[0]);
				}
			}
		}
	}
	
	private static void showHelp(PrintStream o) {
		o.printf("List of commands:\n\n");
		
		o.printf("category -- Show the plugin's category lists\n");
		o.printf("category <catlist> -- Show the categories within the 'catlist' category list\n");
		o.printf("fetch <catlist> <cat> [<count> [<start>]] -- Download 'count' items starting at 'start' from the category 'cat' within the 'catlist' category list\n");
		o.printf("help -- This list\n");
		o.printf("list -- List available plugins\n");
		o.printf("q,quit,exit -- Quit the application\n");
		o.printf("select,use <plugin> -- Make 'plugin' the current plugin\n");
	}

	/**
	 * Download files.
	 * 
	 * @param o System.out
	 * @param cmd Parsed command array
	 */
	private static final void fetch(PrintStream o, String[] cmd) {
		if (currentPlugin == null) {
			System.err.printf("No plugin has been selected!\n");
			return;
		}
		
		try {
			if (cmd.length >= 3) {
				// Get the list of categories
				HashMap<String, CategoryList> categoryList = currentPlugin.getCategoryLists();
				CategoryList list = categoryList.get(cmd[1]);
				if (list != null) {
					// Get the category
					HashMap<String, Category> categories = list.getCategories();
					Category cat = categories.get(cmd[2]);
					if (cat != null) {
						/* Depending on the number of parameters given,
						 * start fetching URLs from the plug-in
						 */
						o.printf("Gathering URLs for downloading...");
						URL[] urls;
						if (cmd.length == 3) {
							urls = cat.fetch(0, -1);
						} else if (cmd.length == 4) {
							try {
								urls = cat.fetch(0, Integer.valueOf(cmd[3]));
							} catch (NumberFormatException nfe) {
								o.printf("Argument 4 is not a valid integer\n");
								return;
							}
						} else if (cmd.length == 5) {
							try {
								urls = cat.fetch(Integer.valueOf(cmd[4]), Integer.valueOf(cmd[3]));
							} catch (NumberFormatException nfe) {
								o.printf("Argument 4 and/or 5 is not a valid integer\n");
								return;
							}
						} else {
							o.printf("Incorrect number of arguments given\n");
							return;
						}
						if (urls != null) {
							o.printf("%d found.\n", urls.length);
						} else {
							o.printf("failed!\n");
							return;
						}
						
						// Pass the URLs on to the FileFetcher
						try {
							String folderName = currentPlugin.getPluginName() + "-" + cat.getName(); 
							FileFetcher.fetch(urls, cat.getFilter(), folderName);
						} catch (PatternSyntaxException pse) {
							o.printf("The plugin provided an invalid regular expression\n");
						}
					} else {
						o.printf("The plugin '%s' has no category '%s' within category type '%s'\n", currentPlugin.getPluginName(), cmd[2], cmd[1]);
					}
				} else {
					o.printf("The plugin '%s' has no category type '%s'\n", currentPlugin.getPluginName(), cmd[1]);
				}
			} else {
				o.printf("Incorrect number of arguments given\n");
			}
		} catch (PluginException e) {
			o.printf("The plugin failed with the following message: '%s'", e.getMessage());
		}
		
	}
	
	/**
	 * List categories.
	 * 
	 * @param o System.out
	 * @param cmd Parsed command array
	 */
	private static final void listCategories(final PrintStream o,String[] cmd) {
		if (currentPlugin == null) {
			System.err.printf("No plugin has been selected!\n");
			return;
		}
		
		// If no arguments are given...
		try {
			final HashMap<String, CategoryList> categoryLists = currentPlugin.getCategoryLists();
			if (cmd.length == 1) {
				// ...list category types.
				o.printf("%s has the following category types:\n", currentPlugin.getPluginName());
				String[] catTypes = new String[categoryLists.size()];
				int catCount = 0;
				for (CategoryList cl : categoryLists.values()) {
					catTypes[catCount++] = "\t" + cl.getType() + " - " + cl.getDescription();
				}
				Arrays.sort(catTypes);
				for (String out : catTypes) {
					o.println(out);
				}
				
			// If category list is given...
			} else if (cmd.length == 2) {
				// ...list categories
					final CategoryList list = categoryLists.get(cmd[1]);
					if (list != null) {
						final HashMap<String, Category> categories = list.getCategories();
						String[] cats = new String[categories.size()];
						int catCount = 0;
						
						for (Category cat : categories.values()) {
							cats[catCount++] = "\t" + cat.getName() + " - " + cat.getDescription();
						}
						Arrays.sort(cats);
						for (String out : cats) {
							o.println(out);
						}
						
					} else {
						o.printf("The plugin '%s' has no category type '%s'", currentPlugin.getPluginName(), cmd[1]);
					}
			} else {
				o.printf("Incorrect number of arguments given\n");
			}
		} catch (PluginException e) {
			o.printf("The plugin failed with the following message: '%s'", e.getMessage());
		}
	}
	
	/**
	 * Select plugin.
	 * 
	 * @param o System.out
	 * @param cmd Parsed command array
	 */
	private static final void selectPlugin(final PrintStream o, String[] cmd) {
		FreeZzaphPlugin plugin = pluginList.get(cmd[1]);
		
		if (plugin != null) {
			currentPlugin = plugin;
			o.printf("Has selected plugin '%s' for use\n", cmd[1]);
		} else {
			o.printf("The plugin '%s' does not exist\n", cmd[1]);
		}
	}
	
	/**
	 * List plugins.
	 * 
	 * @param o System.out
	 * @param cmd Parsed command array
	 */
	private static void listPlugins(final PrintStream o) {
		o.printf("List of installed plugins:\n");
		for (FreeZzaphPlugin plugin : pluginList.values()) {
			try {
				o.printf("\t%s - %s\n", plugin.getPluginName(), plugin.getPluginTitle());
			} catch (Exception e) {
				System.err.printf("The plugin %s threw an exception while listing\n", plugin.getPluginName());
			}
		}
	}

	/**
	 * <p>Picks a string apart into parameters. Will convert a string
	 * like <b>a b "c d e" f<b> into
	 * 
	 * <ol>
	 * 	<li>a</li>
	 * 	<li>b</li>
	 * 	<li>c d e</li>
	 *  <li>f</li>
 	 * </ol>
 	 * 
	 * @param input string to parse
	 * @return array of parsed parameters
	 */
	private static final String[] parseParameters(String input) {
		char[] inputc = input.toCharArray();
		ArrayList<String> cmds = new ArrayList<String>();
		StringBuilder cmd = new StringBuilder();
		boolean isInString = false;
		
		for (int i = 0; i < inputc.length; i++) {
			if (Character.isWhitespace(inputc[i]) && !isInString) {
				cmds.add(cmd.toString());
				cmd.delete(0, cmd.length());
			} else if (inputc[i] == '"') {
				if (!isInString) {
					isInString = true;
				} else {
					cmds.add(cmd.toString());
					cmd.delete(0, cmd.length());
					isInString = false;
				}
			} else {
				cmd.append(inputc[i]);
			}
		}
		
		if (!isInString) {
			if (cmd.length() != 0) {
				cmds.add(cmd.toString());
			}
			return cmds.toArray(new String[0]);
		}
		return null;
		
	}

}
